import aiohttp.web as web
import socketio
import asyncio
import jkzuc
import logging
import traceback

# debug


class WSIBaseException(Exception):
    """
    web socket io 服务异常基类
    """

    def __str__(self) -> str:
        return "WSIBaseException: Unknown web socket io exception"


class WSIPollJKZUCException(WSIBaseException):
    """
    web socket io 更新 jkzuc 状态失败
    """

    def __str__(self) -> str:
        return "WSIPollJKZUCException: jkzuc.stat poll err,perhaps NML of robot dead"


class WSIConfig:
    """
    WSI 模块配置模板

    Attributes:
        UPDATE_INTERVAL 配置从jkzuc stat拉取状态间隔
    """

    def __init__(self):
        self.UPDATE_INTERVAL = 100


def update_jkzuc_status(s):
    """
    更新 jkzuc 状态
    """
    try:
        s.poll()
    except Exception:
        raise WSIPollJKZUCException()


async def peridical_update_io(jkzuc_stat, data, old_data):
    """
    定期更新IO状态
    """

    async def get_old_data(base, key):
        return base.get(key)

    async def get_new_data(jkzuc_sub_stat):
        return [v for v in jkzuc_sub_stat]

    async def update_if_modified(new_value, old_value, key, *stat_list):
        """
        比较 new_value 和 old_value, 如果相等， 则退出
        如果不相等, 则把dest
        """
        if new_value and new_value != old_value:
            for stat in stat_list:
                stat[key] = new_value

    async def update_cab_io_state():
        """
        更新cab io状态
        """
        if old_data.get("cab_io_state") is None:
            old_data["cab_io_state"] = {}

        cab_io_state = {}
        old_cab_io_state = old_data["cab_io_state"]

        di_old_data = await get_old_data(old_cab_io_state, "di")
        di_new_data = await get_new_data(s.cab_din)
        await update_if_modified(
            di_new_data, di_old_data, "di", cab_io_state, old_cab_io_state
        )

        do_old_data = await get_old_data(old_cab_io_state, "do")
        do_new_data = await get_new_data(s.cab_dout)
        await update_if_modified(
            do_new_data, do_old_data, "do", cab_io_state, old_cab_io_state
        )

        ai_old_data = await get_old_data(old_cab_io_state, "ai")
        ai_new_data = await get_new_data(s.cab_ain)
        await update_if_modified(
            ai_new_data, ai_old_data, "ai", cab_io_state, old_cab_io_state
        )

        ao_old_data = await get_old_data(old_cab_io_state, "ao")
        ao_new_data = await get_new_data(s.cab_aout)
        await update_if_modified(
            ao_new_data, ao_old_data, "ao", cab_io_state, old_cab_io_state
        )

        if cab_io_state:
            data["cab_io_state"] = cab_io_state

    async def update_tio_state():
        """
        更新tio状态
        """
        if old_data.get("tio_state") is None:
            old_data["tio_state"] = {}

        tio_state = {}
        old_tio_state = old_data["tio_state"]

        do_old_data = await get_old_data(old_tio_state, "do")
        do_new_data = await get_new_data(s.tio_dout)
        await update_if_modified(
            do_new_data, do_old_data, "do", tio_state, old_tio_state
        )

        di_old_data = await get_old_data(old_tio_state, "di")
        di_new_data = await get_new_data(s.tio_din)
        await update_if_modified(
            di_new_data, di_old_data, "di", tio_state, old_tio_state
        )

        ai_old_data = await get_old_data(old_tio_state, "ai")
        ai_new_data = await get_new_data(s.tio_ain)
        await update_if_modified(
            ai_new_data, ai_old_data, "ai", tio_state, old_tio_state
        )

        if tio_state:
            data["tio_state"] = tio_state

    async def update_ext_io_state():
        """
        更新扩展io状态
        """
        if old_data.get("ext_io_state") is None:
            old_data["ext_io_state"] = {}

        ext_io_state = {}
        old_ext_io_state = old_data["ext_io_state"]

        di_old_data = await get_old_data(old_ext_io_state, "di")
        di_new_data = await get_new_data(s.extio_stat[0])
        await update_if_modified(
            di_new_data, di_old_data, "di", ext_io_state, old_ext_io_state
        )

        do_old_data = await get_old_data(old_ext_io_state, "do")
        do_new_data = await get_new_data(s.extio_stat[1])
        await update_if_modified(
            do_new_data, do_old_data, "do", ext_io_state, old_ext_io_state
        )

        ai_old_data = await get_old_data(old_ext_io_state, "ai")
        ai_new_data = await get_new_data(s.extio_stat[2])
        await update_if_modified(
            ai_new_data, ai_old_data, "ai", ext_io_state, old_ext_io_state
        )

        ao_old_data = await get_old_data(old_ext_io_state, "ao")
        ao_new_data = await get_new_data(s.extio_stat[3])
        await update_if_modified(
            ao_new_data, ao_old_data, "ao", ext_io_state, old_ext_io_state
        )

        if ext_io_state:
            data["ext_io_state"] = ext_io_state

    async def update_mb_slave_io():
        AI_AO_UINT_START_INDEX = 0
        AI_AO_UINT_NUM = 16
        AI_AO_INT_START_INDEX = 16
        AI_AO_INT_NUM = 16
        AI_AO_FLOAT_START_INDEX = 32

        if old_data.get("mb_slave_io_state") is None:
            old_data["mb_slave_io_state"] = {}

        mb_slave_io_state = {}
        old_mb_slave_io_state = old_data["mb_slave_io_state"]

        di_old_data = await get_old_data(old_mb_slave_io_state, "di")
        di_new_data = await get_new_data(s.mb_slave_din)
        await update_if_modified(
            di_new_data, di_old_data, "di", mb_slave_io_state, old_mb_slave_io_state
        )

        do_old_data = await get_old_data(old_mb_slave_io_state, "do")
        do_new_data = await get_new_data(s.mb_slave_dout)
        await update_if_modified(
            do_new_data, do_old_data, "do", mb_slave_io_state, old_mb_slave_io_state
        )

        ai_uint_old_data = await get_old_data(old_mb_slave_io_state, "ai_uint")
        ai_uint_new_data = await get_new_data(
            s.mb_slave_ain[
                AI_AO_UINT_START_INDEX : AI_AO_UINT_START_INDEX + AI_AO_UINT_NUM
            ]
        )
        await update_if_modified(
            ai_uint_new_data,
            ai_uint_old_data,
            "ai_uint",
            mb_slave_io_state,
            old_mb_slave_io_state,
        )

        ai_int_old_data = await get_old_data(old_mb_slave_io_state, "ai_int")
        ai_int_new_data = await get_new_data(
            s.mb_slave_ain[
                AI_AO_INT_START_INDEX : AI_AO_INT_START_INDEX + AI_AO_INT_NUM
            ]
        )
        await update_if_modified(
            ai_int_new_data,
            ai_int_old_data,
            "ai_int",
            mb_slave_io_state,
            old_mb_slave_io_state,
        )

        ai_float_old_data = await get_old_data(old_mb_slave_io_state, "ai_float")
        ai_float_new_data = await get_new_data(s.mb_slave_ain[AI_AO_FLOAT_START_INDEX:])
        await update_if_modified(
            ai_float_new_data,
            ai_float_old_data,
            "ai_float",
            mb_slave_io_state,
            old_mb_slave_io_state,
        )

        ao_uint_old_data = await get_old_data(old_mb_slave_io_state, "ao_uint")
        ao_uint_new_data = await get_new_data(
            s.mb_slave_aout[
                AI_AO_UINT_START_INDEX : AI_AO_UINT_START_INDEX + AI_AO_UINT_NUM
            ]
        )
        await update_if_modified(
            ao_uint_new_data,
            ao_uint_old_data,
            "ao_uint",
            mb_slave_io_state,
            old_mb_slave_io_state,
        )

        ao_int_old_data = await get_old_data(old_mb_slave_io_state, "ao_int")
        ao_int_new_data = await get_new_data(
            s.mb_slave_aout[
                AI_AO_INT_START_INDEX : AI_AO_INT_START_INDEX + AI_AO_INT_NUM
            ]
        )
        await update_if_modified(
            ao_int_new_data,
            ao_int_old_data,
            "ao_int",
            mb_slave_io_state,
            old_mb_slave_io_state,
        )

        ao_float_old_data = await get_old_data(old_mb_slave_io_state, "ao_float")
        ao_float_new_data = await get_new_data(
            s.mb_slave_aout[AI_AO_FLOAT_START_INDEX:]
        )
        await update_if_modified(
            ao_float_new_data,
            ao_float_old_data,
            "ao_float",
            mb_slave_io_state,
            old_mb_slave_io_state,
        )

        if mb_slave_io_state:
            data["mb_slave_io_state"] = mb_slave_io_state

    async def update_profinet_io():
        # TODO:
        pass

    async def update_eip_io():
        # TODO:
        pass

    s = jkzuc_stat
    rc_logger.info("update io start !")

    await update_cab_io_state()
    await update_tio_state()
    await update_ext_io_state()
    await update_mb_slave_io()
    await update_profinet_io()
    await update_eip_io()

    rc_logger.info("update io end !")


async def peridical_update_program(jkzuc_stat, data, old_data):
    """
    定期更新程序状态
    """
    rc_logger.info("update program start !")
    rc_logger.info("update program end !")


async def peridical_update(app):
    """
    周期性发送状态
    """
    global __UPDATE_PERIDICAL_LOOP
    idx = 0

    s = None
    old_data = {}

    while __UPDATE_PERIDICAL_LOOP:
        # 检查 s 是否为空，若为空，则创建
        if s is None:
            try:
                s = jkzuc.stat()
            except Exception:
                rc_logger.error(e)
                s = None
                await asyncio.sleep(1)
                continue

        if len(client_sids) <= 0:
            # 连接客户端数量为0时，停止发送
            await asyncio.sleep(0.05)
            continue

        try:
            update_jkzuc_status(s)
        except Exception as e:
            rc_logger.error(e)
            s = None
            await asyncio.sleep(1)
            continue

        # 拼接需要被发送的包
        data = {}

        # 以协程方式创建伪并行任务
        # 更新策略：仅发送差分数据
        furture_handler = [
            peridical_update_io(s, data, old_data),
            # peridical_update_program(s, data, old_data),
        ]
        await asyncio.gather(*furture_handler)

        if data == {}:
            asyncio.sleep(wsi_config.UPDATE_INTERVAL)
            continue

        data["idx"] = idx
        idx += 1

        try:
            # 发送数据到客户端
            rc_logger.info("send data: {}".format(data))
            await asio.emit("io", data=data)
        except asyncio.CancelledError:
            break
        except Exception as e:
            rc_logger.error(traceback.format_exc())


# TODO: 这部分需要整理下
# 当前模块基础日志配置
base_logger = logging.getLogger("WSI")
base_logger.setLevel(logging.DEBUG)

# TODO: 日志输出到wsi.log
# 定义日志输出到终端
console_handler = logging.StreamHandler()
formatter = logging.Formatter("%(asctime)s / %(name)s / %(levelname)s / %(message)s")
console_handler.setFormatter(formatter)

base_logger.addHandler(console_handler)

# 继承WSI日志配置，用于输出socketio库内置模块engineio的日志
engineio_logger = logging.getLogger("WSI.eng")

# 继承WSI日志配置，输出当前模块的日志
rc_logger = logging.getLogger("WSI.rc")

# 继承WSI日志配置，输出socketio库内部日志
sio_logger = logging.getLogger("WSI.sio")

# wsi 模块配置
wsi_config = WSIConfig()

# 创建异步socketio服务器
asio = socketio.AsyncServer(
    logger=sio_logger,
    async_mode="aiohttp",
    always_connect=True,
    engineio_logger=engineio_logger,
    cors_allowed_origins="*",
)

app = web.Application()

# 定义后台异步周期任务
data_listener = web.AppKey("data-listener", asyncio.Task[None])

asio.attach(app)

# 表示是否退出周期更新任务
__UPDATE_PERIDICAL_LOOP = False

# 客户端 sid 列表
client_sids = []


@asio.event
async def login(sid, data):
    """
    自定义登录函数，响应登录事件
    """
    try:
        username = data["username"]
        usertoken = data["usertoken"]

        # TODO: 登录验证

    except Exception:
        asio.emit("login_success", data={"ecode": 0})
    else:
        asio.emit("login_fail", data={"ecode": 1})


@asio.event
async def connect(sid, environ, auth):
    """
    处理客户端连接
    """
    client_sids.append(sid)
    rc_logger.info("connect {}".format({"sid": sid, "auth": auth}))


@asio.event
async def disconnect(sid):
    """
    处理客户端连接断开
    """
    rc_logger.info("disconnect {}".format(sid))
    client_sids.remove(sid)


@asio.event
async def connect_error(data):
    """
    处理连接中报错问题
    """
    rc_logger.info("connect error {}".format({"data": data}))


@asio.on("*")
async def any_event(event, sid, data):
    """
    默认事件处理函数
    """
    rc_logger.info(
        "unknown event {}".format({"event": event, "sid": sid, "data": data})
    )


async def init_app():
    async def peridical_update_task(app):
        """
        包装周期性更新任务
        """
        global __UPDATE_PERIDICAL_LOOP
        global client_sids_cond

        __UPDATE_PERIDICAL_LOOP = True
        app[data_listener] = asyncio.create_task(peridical_update(app))

        yield

        __UPDATE_PERIDICAL_LOOP = False
        app[data_listener].cancel()
        await app[data_listener]

    # 添加异步后台任务
    app.cleanup_ctx.append(peridical_update_task)
    return app


def socket_io_server():
    web.run_app(init_app(), port=5000)


if __name__ == "__main__":
    web.run_app(init_app(), port=5000)
